package com.xbongbong.paas.redis;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.xbongbong.paas.constant.TimeConstant;
import com.xbongbong.paas.toolbox.util.CollectionsUtil;
import com.xbongbong.paas.toolbox.util.MD5Util;
import com.xbongbong.paas.toolbox.util.StringUtil;
import com.xbongbong.paas.toolbox.wrap.BaseDTO;
import com.xbongbong.saas.toolbox.help.FastJsonHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * PAAS 项目 redis 工具类
 * @author kaka
 * @date 2018-9-6 10:47
 */
@Component
public class PaasRedisHelper {

    private static final Logger LOG = LoggerFactory.getLogger(PaasRedisHelper.class);

    /**
     * 存在redis的默认有效时间，单位秒,设置为1个小时
     */
    public static final int DEFAULT_EFFECTIVE_TIME = TimeConstant.SECONDS_PER_HOUR;

    /**
     * 并发锁时间, 默认10秒
     */
    private static final int CONCURRENT_LOCK_TIME = 10;

    /**
     * 镜像安装-并发锁时间, 默认120秒
     */
    private static final int CONCURRENT_LOCK_TIME_MIRROR = 120;

    /**
     * 购物车失效时间默认 3个月（2592000秒）
     */
    private static final int CART_EXPIRES_TIME = 2592000;

    /**
     * 并发锁存入的value值
     */
    private static final String CONCURRENT_LOCK_VALUE = "lock";

    /**
     * 根据DTO获取哈希值时忽略部分字段
     */
    private static final String[] CONCURRENT_EXCLUDE_FIELDS = {
            "browserInfo",
            "httpHeader",
            "loginUser",
            "ip",
            "ignoreConCheck"
    };

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 项目名，redis的第一级前缀
     */
    @Value("${com.xbongbong.pro.main.name}")
    private String projectName;

    /**
     * saas项目名，用于suiteToken和suiteTicket的项目前缀
     * 若saasName 和 name相同一般用于单独部署，saasName与name不同则一般用于与saas项目联合部署
     */
    @Value("${com.xbongbong.pro.main.saasName}")
    private String saasProjectName;


    /**
     * 获取redis自增值，会进行一次自增，增量为delta
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param cacheSeconds 缓存秒数，若 <= 0 则为永久有效[如无必要尽量不要存永久有效的值]
     * @param initValue 初始值
     * @param delta 自增增量，>= 1
     * @return 自增增量本次自增后的值
     */
    public Long getIncrNum(String filePrefix, String key, int cacheSeconds, int initValue, int delta) throws IllegalArgumentException {
        return getIncrNum(filePrefix, key, cacheSeconds, initValue, delta, TimeUnit.SECONDS);
    }



    /**
     * 获取redis自增值，会进行一次自增，增量为delta
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param timeout 有效时间，单位根据timeUnit
     * @param initValue 初始值
     * @param delta 自增增量，>= 1
     * @param timeUnit TimeUnit单位
     * @return 自增增量本次自增后的值
     */
    public Long getIncrNum(String filePrefix, String key, int timeout, int initValue, int delta, TimeUnit timeUnit) throws IllegalArgumentException {
        if (StringUtil.isEmpty(filePrefix) || StringUtil.isEmpty(key) || initValue < 0) {
            throw new IllegalArgumentException();
        }
        Long value;
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try {
            value = stringRedisTemplate.opsForValue().increment(realKey, delta);
            if (value == null) {
                return 0L;
            }
            //第一次设值处理设置对应的key的缓存时间
            if (value == delta) {
                //第一次设置有效期
                if (timeout > 0){
                    stringRedisTemplate.expire(realKey, timeout, timeUnit);
                }
            }
            //此方法返回单位为秒过期时长，返回值为-1时 此键值没有设置过期日期，返回值为-2时 不存在此键
            Long seconds = stringRedisTemplate.getExpire(realKey, timeUnit);
            //当key没有设置有效时间，将有效时间更新（防止出现没有有效时间的异常数据）
            if (timeout > 0 && Objects.equals(seconds, -1L)) {
                stringRedisTemplate.expire(realKey, timeout, timeUnit);
            }
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        return value + initValue - delta;
    }
    /**
     * 获取redis自增值，会进行一次自增，增量为1
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param cacheSeconds 缓存秒数，若 <= 0 则为永久有效[如无必要尽量不要存永久有效的值]
     * @param initValue 初始值
     * @return 自增增量本次自增后的值
     */
    public Long getIncrNumByOne(String filePrefix, String key, int cacheSeconds, int initValue) throws IllegalArgumentException {
        return getIncrNum(filePrefix, key, cacheSeconds, initValue, 1);
    }


    /**
     * 获取redis自增值，会进行一次自增，增量为1, 初始值为0
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param cacheSeconds 缓存秒数，若 <= 0 则为永久有效[如无必要尽量不要存永久有效的值]
     * @return 自增增量本次自增后的值
     */
    public Long getIncrNumByOne(String filePrefix, String key, int cacheSeconds) throws IllegalArgumentException {
        return getIncrNum(filePrefix, key, cacheSeconds, 1, 1);
    }


    /**
     * 获取redis自增值，会进行一次自增，增量为1, 初始值为0
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param timeout 缓存时间，单位为 timeUnit
     * @param timeUnit TimeUnit
     * @return 自增增量本次自增后的值
     */
    public Long getIncrNumByOne(String filePrefix, String key, int timeout, TimeUnit timeUnit) throws IllegalArgumentException {
        return getIncrNum(filePrefix, key, timeout, 1, 1, timeUnit);
    }

    /**
     * 从redis中获取 projectName + ":" + filePrefix + ":" + key 对应的 值
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @return redis 中 realKey 对应的值，String类型
     */
    public String getValue(String filePrefix, String key) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        return getValue(realKey);
    }

    /**
     * 设置redis值
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key key 缓存key最后一部分
     * @param json 需要存储的值，若为String类型则直接存储，若为其他则通过JSON.toJSONString转为String
     * @param time 缓存时效，单位为秒
     */
    public void setValue(String filePrefix, String key, Object json, Integer time) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try {
            String jsonStr = objectValueToString(json);
            if (time != null && time > 0) {
                // 如果有时间传入，则传入时间
                stringRedisTemplate.opsForValue().set(realKey, jsonStr, time, TimeUnit.SECONDS);
            } else {
                stringRedisTemplate.opsForValue().set(realKey, jsonStr, DEFAULT_EFFECTIVE_TIME, TimeUnit.SECONDS);
            }
        } catch (Exception e) {
            LOG.error("redis写入" + realKey + "时出错", e);
            throw e;
        }
    }

    /**
     * 设置set类型的redis值
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key key 缓存key最后一部分
     * @param setValue set集合
     * @param time 缓存时效，单位为秒
     */
    public void setValue4Set(String filePrefix, String key, Set setValue, Integer time) {
        setValue(filePrefix, key, setValue, time);
    }

    /**
     * 从redis获取Set类型的值
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key key 缓存key最后一部分
     * @return
     */
    public Set getValue4Set(String filePrefix, String key) {
        String longSetStr = getValue(filePrefix, key);
        Set  setValue = JSON.parseObject(longSetStr, Set.class);
        return setValue;
    }

    /**
     * 获取全部set值
     * 与getValue4Set()方法区别：getValue4Set以String类型存redis，本方法以set类型存redis
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @return 全部set值
     */
    public Set getValue4Set2(String filePrefix, String key) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        Set members = stringRedisTemplate.opsForSet().members(realKey);
        return members;
    }

    /**
     * 判断某个value是否在Set集合中
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param value 需判断的value
     * @return true：该value在redis集合中
     *         false：该value不在redis集合中
     */
    public Boolean isMember(String filePrefix, String key, String value) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        return stringRedisTemplate.opsForSet().isMember(realKey, value);
    }

    /**
     * 设置redis set
     * 与setValue4Set()区别：setValue4Set() 以String类型存redis，本方法以set存redis
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param setValue set集合
     * @param time 过期时间
     */
    public void addValue4Set(String filePrefix, String key, Set<String> setValue, Integer time) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try {
            String[] values = objectValue2Array(setValue);
            if (time != null && time > 0) {
                // 如果有时间传入，则传入时间
                stringRedisTemplate.opsForSet().add(realKey, values);
                expire(filePrefix, key, time);
            } else {
                stringRedisTemplate.opsForSet().add(realKey, values);
                expire(filePrefix, key,  DEFAULT_EFFECTIVE_TIME);
            }
        } catch (Exception e) {
            LOG.error("redis写入" + realKey + "时出错", e);
            throw e;
        }
    }

    public void addValue(String filePrefix, String key,Collection<String> value, Integer time){
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try {
            String[] values = objectValue4Array(value);
            if (time != null && time > 0) {
                // 如果有时间传入，则传入时间
                stringRedisTemplate.opsForSet().add(realKey, values);
                expire(filePrefix, key, time);
            } else {
                stringRedisTemplate.opsForSet().add(realKey, values);
                expire(filePrefix, key, CONCURRENT_LOCK_TIME_MIRROR);
            }
        }catch (Exception e){
            LOG.error("redis写入" + realKey + "时出错", e);
            throw e;
        }
    }
    /**
     * 设置redis set
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param setValue set集合
     */
    public void removeValue4Set(String filePrefix, String key, List setValue) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try {
            String[] array = new String[setValue.size()];
            for (int i = 0; i < setValue.size(); i++) {
                array[i] = setValue.get(i).toString();
            }
            stringRedisTemplate.opsForSet().remove(realKey, array);
        } catch (Exception e) {
            LOG.error("redis写入" + realKey + "时出错", e);
            throw e;
        }
    }



    /**
     * 目前只用于suiteToken 和 suiteTicket, 相关token为持久化存储
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key key 缓存key最后一部分
     * @param json 需要存储的值，若为String类型则直接存储，若为其他则通过JSON.toJSONString转为String
     */
    public void setSuiteTokenValue(String filePrefix, String key, Object json) {
        String realKey = saasProjectName + ":" + filePrefix + ":" + key;
        setValuePermanent(realKey, json);
    }

    /**
     * 目前只用于suiteToken 和 suiteTicket, 相关值对应的缓存获取
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @return redis 中 realKey 对应的值，String类型
     */
    public String getSuiteTokenValue(String filePrefix, String key) {
        String realKey = saasProjectName + ":" + filePrefix + ":" + key;
        return getValue(realKey);
    }

    /**
     * 谨慎使用本方法，除suitToken, 公司的持久码等少数几个需要持久缓存的数据，其他数据非不得己不要使用本方法存缓存
     * 设置redis值, 不设置超时时间
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key key 缓存key最后一部分
     * @param json 需要存储的值，若为String类型则直接存储，若为其他则通过JSON.toJSONString转为String
     */
    public void setValuePermanent(String filePrefix, String key, Object json) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        setValuePermanent(realKey, json);
    }

    /**
     *  从redis中删除某个key
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key key 缓存key最后一部分
     */
    public void removeValue(String filePrefix, String key) {
        String realKey = projectName + ":"  + filePrefix + ":" + key;
        try {
            stringRedisTemplate.delete(realKey);
        } catch (Exception e) {
            LOG.error("redis删除key：" + realKey + "时出错", e);
            throw e;
        }
    }

    /**
     *  从redis中删除大key下的某个小key
     * @param filePrefix 缓存key前缀
     * @param key1 大key的一部分 一般为userId
     * @param key2 小key 删除的目标key
     */
    public void removeHashValue(String filePrefix, String key1, Object key2) {
        String realKey = projectName + ":" + filePrefix + ":" + key1;
        try {
            stringRedisTemplate.opsForHash().delete(realKey, key2);
        } catch (Exception e) {
            LOG.error("redis删除key：" + realKey + "中的" + key2 + "时出错", e);
            throw e;
        }
    }

    /**
     * 从redis中批量删除多个key, 传入的是RedisKeyPojo
     * @param keyPojos RedisKeyPojo的集合，用以拼装需要删除的key
     */
    public void removeValue(Collection<RedisKeyPojo> keyPojos) {
        if (CollectionsUtil.isEmpty(keyPojos)) {
            return;
        }
        List<String> realKeys = new ArrayList<>();
        for (RedisKeyPojo keyPojo : keyPojos) {
            String realKey = projectName + ":"  + keyPojo.getPrefix() + ":" + keyPojo.getLastKey();
            realKeys.add(realKey);
        }
        removeRealValue(realKeys);
    }

    /**
     * 获取并发锁，若没有被占用则返回true并构建锁，若已被占用则返回false
     * 并发锁时间为 seconds 秒
     *
     * @param prefix    redis 并发前缀
     * @param reqJson   表单提交DTO对应的内容Json String
     * @param seconds   并发锁存活时间，单位秒
     * @param doHash    计算reqJson时是否进行hash,若为true,则进行md5哈希，若为false则不进行哈希运算
     * @return true表示正常，false表示被锁定
     *
     * @author kaka
     * @date 2018-9-21 14:04
     */
    public boolean checkConcurrentLock(String prefix, String reqJson, Integer seconds, boolean doHash) {
        // 取MD5签名
        String key;
        if (doHash) {
            key = MD5Util.EncoderByMd5(reqJson);
        } else {
            key =  reqJson;
        }
        String realKey = projectName + ":" + prefix + ":" + key;
        Boolean ret;
        try {
            ret = stringRedisTemplate.opsForValue().setIfAbsent(realKey, CONCURRENT_LOCK_VALUE, seconds, TimeUnit.SECONDS);
        } catch (Exception e) {
            LOG.error("redis 取出" + realKey + "时出错", e);
            throw e;
        }
        if (ret == null) {
            return false;
        }
        return ret;
    }

    /**
     * 获取并发锁，若没有被占用则返回true并构建锁，若已被占用则返回false
     * 默认并发锁时间为5秒
     *
     * @param prefix    redis 并发前缀
     * @param reqJson   表单提交DTO对应的内容Json String
     * @param doHash    计算reqJson时是否进行hash,若为true,则进行md5哈希，若为false则不进行哈希运算
     * @return true表示正常，false表示被锁定
     *
     */
    public boolean checkConcurrentLock(String prefix, String reqJson, boolean doHash) {
        return checkConcurrentLock(prefix, reqJson, CONCURRENT_LOCK_TIME, doHash);
    }

    /**
     * 获取并发锁，若没有被占用则返回true并构建锁，若已被占用则返回false
     * 默认并发锁时间为5秒, 默认前缀为 {@link RedisPrefixConstant#PAAS_CONCURRENT_LOCK}
     * 计算key值时对reqJson执行md5哈希
     *
     * @param reqJson     表单提交DTO对应的内容Json String
     * @return true表示正常，false表示被锁定
     *
     */
    public boolean checkConcurrentLock(String reqJson) {
        return checkConcurrentLock(RedisPrefixConstant.PAAS_CONCURRENT_LOCK, reqJson, true);
    }

    /**
     * 获取并发锁，若没有被占用则返回true并构建锁，若已被占用则返回false
     * 默认并发锁时间为5秒, 默认前缀为 {@link RedisPrefixConstant#PAAS_CONCURRENT_LOCK}
     * 计算key值时对reqJson执行md5哈希
     *
     * @param jsonObj     表单提交DTO实体
     * @return true表示正常，false表示被锁定
     *
     */
    public boolean checkConcurrentLock(Object jsonObj) {
        return checkConcurrentLock(JSON.toJSONString(jsonObj));
    }

    /**
     * 获取并发锁，若没有被占用则返回true并构建锁，若已被占用则返回false
     * 默认并发锁时间为5秒, 默认前缀为 {@link RedisPrefixConstant#PAAS_CONCURRENT_LOCK}
     * 计算key值时对reqJson执行md5哈希
     *
     * @param dto     表单提交DTO实体
     * @return true表示正常，false表示被锁定
     *
     */
    public boolean checkConcurrentLock(BaseDTO dto) {
        String serializeJson = FastJsonHelper.parseExcludeObject(CONCURRENT_EXCLUDE_FIELDS, dto);
        return checkConcurrentLock(serializeJson);
    }

    /**
     * 释放并发锁
     * 默认前缀为 {@link RedisPrefixConstant#PAAS_CONCURRENT_LOCK}
     * 计算key值时对reqJson执行md5哈希
     *
     * @param jsonObj     表单提交DTO实体
     *
     */
    public void releaseConcurrentLock(Object jsonObj) {
        releaseConcurrentLock(JSON.toJSONString(jsonObj));
    }

    /**
     * 释放并发锁
     * 默认前缀为 {@link RedisPrefixConstant#PAAS_CONCURRENT_LOCK}
     * 计算key值时对reqJson执行md5哈希
     *
     * @param dto     表单提交DTO实体
     *
     */
    public void releaseConcurrentLock(BaseDTO dto) {
        String serializeJson = FastJsonHelper.parseExcludeObject(CONCURRENT_EXCLUDE_FIELDS, dto);
        releaseConcurrentLock(serializeJson);
    }

    /**
     * 释放并发锁
     * 默认前缀为 {@link RedisPrefixConstant#PAAS_CONCURRENT_LOCK}
     * 计算key值时对reqJson执行md5哈希
     *
     * @param reqJson     表单提交DTO对应的内容Json String
     */
    public void releaseConcurrentLock(String reqJson) {
        releaseConcurrentLock(RedisPrefixConstant.PAAS_CONCURRENT_LOCK, reqJson, true);
    }

    /**
     * 释放并发锁
     *
     * @param prefix    redis 并发前缀
     * @param reqJson   表单提交DTO对应的内容Json String
     * @param doHash    计算reqJson时是否进行hash,若为true,则进行md5哈希，若为false则不进行哈希运算
     * @return true表示正常，false表示被锁定
     *
     * @author kaka
     */
    public void releaseConcurrentLock(String prefix, String reqJson, boolean doHash) {
        // 取MD5签名
        String key;
        if (doHash) {
            key = MD5Util.EncoderByMd5(reqJson);
        } else {
            key =  reqJson;
        }
        String realKey = projectName + ":" + prefix + ":" + key;

        try {
            stringRedisTemplate.delete(realKey);
        } catch (Exception e) {
            LOG.error("redis 删除key" + realKey + "时出错", e);
            throw e;
        }
    }

    //==================== private method ====================


    /**
     * 从redis中批量删除多个key,需要已经拼装好完整key
     * @param keys 需要批量删除的key集合，请确认是完整key；可以借助方法
     */
    private void removeRealValue(Collection<String> keys) {
        if (CollectionsUtil.isEmpty(keys)) {
            return;
        }
        try {
            stringRedisTemplate.delete(keys);
        } catch (Exception e) {
            LOG.error("redis删除keys：" + keys + "时出错", e);
            throw e;
        }
    }

    /**
     * 从redis中获取 realKey 对应的 值
     * @param realKey 实际redisKey 已经包含各种前缀
     * @return redis 中 realKey 对应的值，String类型
     */
    private String getValue(String realKey) {
        String value;
        try {
            value = stringRedisTemplate.opsForValue().get(realKey);
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        return value;
    }
    /**
     * 从redis中获取 realKey 对应的 值
     * @param filePrefix 实际redisKey 已经包含各种前缀
     * @return redis 中 realKey 对应的值，String类型
     */
    public String getMembers(String filePrefix, String key) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        String value;
        try {
            Set<String> members = stringRedisTemplate.opsForSet().members(realKey);
            if (CollectionsUtil.isEmpty(members)){
                return "";
            }
            value = JSONObject.toJSONString(members);
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        return value;
    }

    /**
     * 设置永久有效的redis键值，直接通过realKey, value形式set
     * @param realKey 实际redisKey 已经包含各种前缀
     * @param json 设置的值
     */
    private void setValuePermanent(String realKey, Object json) {
        try {
            String jsonStr = objectValueToString(json);
            stringRedisTemplate.opsForValue().set(realKey, jsonStr);

        } catch (Exception e) {
            LOG.error("redis写入" + realKey + "时出错", e);
            throw e;
        }
    }

    /**
     * 将 Object 转化为 String，是redis set动作的前置动作
     * @param json 需要转化的value
     * @return 转化为String的值
     */
    private String objectValueToString(Object json) {
        String jsonStr;
        if (json instanceof String) {
            jsonStr = (String) json;
        } else if(json instanceof Integer) {
            jsonStr = String.valueOf(json);
        } else {
            jsonStr = JSON.toJSONString(json, SerializerFeature.DisableCircularReferenceDetect);
        }
        return jsonStr;
    }

    /**
     * 将 Object 转化为 数组，是redis set动作的前置动作
     * @param setData 集合数据
     * @return 转化的数组
     */
    private String[] objectValue2Array(Set<String> setData) {
        String[] array;
        if (Objects.nonNull(setData) && setData.size() > 0) {
            array = setData.toArray(new String[setData.size()]);
        } else {
            array = new String[0];
        }
        return array;
    }

    private String[] objectValue4Array(Collection<String> setData) {
        String[] array;
        if (CollectionsUtil.isNotEmpty(setData)) {
            array = setData.toArray(new String[setData.size()]);
        } else {
            array = new String[0];
        }
        return array;
    }

    /**
     * 设置key的过期时间
     * @param filePrefix
     * @param key
     * @param time
     */
    public void expire(String filePrefix, String key, int time) {
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;
        if (time > 0){
            stringRedisTemplate.expire(realKey, time, TimeUnit.SECONDS);
        }
    }

    /**
     * 获取key的剩余秒数
     * @param filePrefix 前缀
     * @param key key值
     * @return 返回获取redis key的过期时间，剩余秒数
     */
    public Long getExpire(String filePrefix, String key) {
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;
        return stringRedisTemplate.getExpire(realKey);
    }

    /**
     * Description: 获取匹配到的所有key值
     * @author 魏荣杰
     * @date 2020/7/22 13:44
     */
    public Set<String> getKeys(String filePrefix, String key) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        return stringRedisTemplate.keys(realKey);
    }

    /**
     * 判断是否存在key
     * @param filePrefix 前缀
     * @param key key值
     * @return true/false
     * @author zhouwq
     * @date 2020/9/7 14:20
     */
    public Boolean hasKey(String filePrefix, Object key) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        return stringRedisTemplate.hasKey(realKey);
    }

    /**
     * hash结构判断是否存在小key
     * @param filePrefix 前缀
     * @param key1 大key值
     * @param key2 小key值
     * @return true/false
     * @author zhouwq
     * @date 2020/9/7 14:20
     */
    public Boolean hasHashKey(String filePrefix, Object key1, Object key2) {
        String realKey = projectName + ":" + filePrefix + ":" + key1;
        return stringRedisTemplate.opsForHash().hasKey(realKey,key2);
    }

    /**
     * 返回指定hash key的长度
     * @param filePrefix 前缀
     * @return true/false
     * @author weiming
     * @date 2020/12/28 20：43
     */
    public Long getHashSize(String filePrefix, Object key) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        return stringRedisTemplate.opsForHash().size(realKey);
    }

    /**
     * 获取当前key对应的散列表中所有的field和value
     * @param filePrefix 前缀
     * @param key key值
     * @return map
     * @author zhouwq
     * @date 2020/9/7 14:22
     */
    public Map<Object, Object> getAllHashValue(String filePrefix, Object key) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        Map<Object, Object> value;
        try {
            value = stringRedisTemplate.opsForHash().entries(realKey);
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        return value;
    }

    /**
     * 设置散列表中的值 （putAll）
     * @param filePrefix 前缀
     * @param key key值
     * @param map field-value
     * @param time 过期时间
     */
    public void setAllHashValue(String filePrefix, String key, Map<Object, Object> map, Integer time) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try {
            stringRedisTemplate.opsForHash().putAll(realKey, map);
            if (time != null && time > 0) {
                stringRedisTemplate.expire(realKey, time, TimeUnit.SECONDS);
            } else {
                stringRedisTemplate.expire(realKey, DEFAULT_EFFECTIVE_TIME, TimeUnit.SECONDS);
            }
        } catch (Exception e) {
            LOG.error("redis写入" + realKey + "时出错", e);
            throw e;
        }
    }


    //TODO 添加key下的单个数据

    /**
     * 设置散列表中的值 （添加key下的单个数据 put） eg:购物车需要3个月
     * @param filePrefix 前缀
     * @param key1 大key值
     * @param key2 小key值
     * @param object value
     * @param time 过期时间
     */
    public void setHashValue(String filePrefix, String key1, String key2, Object object, Integer time) {
        String realKey = projectName + ":" + filePrefix + ":" + key1;
        try {
            stringRedisTemplate.opsForHash().put(realKey, key2, object);
            if (time != null && time > 0) {
                stringRedisTemplate.expire(realKey, time, TimeUnit.SECONDS);
            } else {
                stringRedisTemplate.expire(realKey, DEFAULT_EFFECTIVE_TIME, TimeUnit.SECONDS);
            }
        } catch (Exception e) {
            LOG.error("redis写入" + realKey + "时出错", e);
            throw e;
        }
    }

    /**
     * 设置散列表中的值 （添加key下的单个数据 put） eg:购物车需要3个月
     * @param filePrefix 前缀
     * @param key1 大key值
     * @param testMap 批量添加的数据
     */
    public void setMapHashValue(String filePrefix, String key1, Map<Object,Object> testMap){
        String realKey = projectName + ":" + filePrefix + ":" + key1;
        try {
            stringRedisTemplate.opsForHash().putAll(realKey, testMap);
        } catch (Exception e) {
            LOG.error("redis写入" + realKey + "时出错", e);
            throw e;
        }
    }


    /**
     * 获取当前key对应的散列表中指定field的value
     * @param filePrefix 前缀
     * @param key1 大key值
     * @param key2 小key值
     * @return map
     * @author weiming
     * @date 2020/12/25 19：28
     */
    public Object getHashValue(String filePrefix, Object key1, Object key2) {
        String realKey = projectName + ":" + filePrefix + ":" + key1;
        Object value;
        try {
            value = stringRedisTemplate.opsForHash().get(realKey, key2);
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        return value;
    }

    /**
     * 获取当前key对应的散列表中指定小key集合的value
     * @param filePrefix 前缀
     * @param key 大key值
     * @param collection 小key的集合
     * @return map
     * @author weiming
     * @date 2020/12/25 19：28
     */
    public List<Object> getMultiHashValue(String filePrefix, Object key, Collection collection) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        List<Object> value;
        try {
            value = stringRedisTemplate.opsForHash().multiGet(realKey, collection);
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        value.removeIf(Objects::isNull);
        return value;
    }

    /**
     * 设置散列表的失效时间  eg:购物车修改商品时
     * @param filePrefix 前缀
     * @param key1 大key值
     * @param time 过期时间
     */
    public void updateHashTime(String filePrefix, String key1, Integer time) {
        String realKey = projectName + ":" + filePrefix + ":" + key1;
        try {
            if (time != null && time > 0) {
                stringRedisTemplate.expire(realKey, time, TimeUnit.SECONDS);
            } else {
                stringRedisTemplate.expire(realKey, CART_EXPIRES_TIME, TimeUnit.SECONDS);
            }
        } catch (Exception e) {
            LOG.error("redis设置" + realKey + "的失效时间时出错", e);
            throw e;
        }
    }

    /**
     * 批量删除当前key对应的散列表中指定field的value
     * @param filePrefix 前缀
     * @param key 大key中的userId
     * @param objects 小key值
     * @return map
     * @author weiming
     * @date 2020/12/25 19：28
     */
    public void delMultiValue(String filePrefix, Object key, Object... objects) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try {
            stringRedisTemplate.opsForHash().delete(realKey,objects);
        } catch (Exception e) {
            LOG.error("从redis删除" + realKey + "下的数据时出错", e);
            throw e;
        }
    }


    /**
     * 获取redis自减值，会进行一次自减，减量为delta
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param delta 自减减量，>= 1
     * @return 自减减量本次自减后的值
     */
    public Long getDecrNum(String filePrefix, String key, int delta) throws IllegalArgumentException {
        if (StringUtil.isEmpty(filePrefix) || StringUtil.isEmpty(key)) {
            throw new IllegalArgumentException();
        }
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;

        Long value;
        try {
            value = stringRedisTemplate.opsForValue().decrement(realKey, delta);
            if (value == null) {
                return 0L;
            }
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        return value;
    }
    /**
     * 获取redis自增值，会进行一次自增，增量为delta
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key 缓存key最后一部分
     * @param delta 自增增量，>= 1
     * @return 自增增量本次自增后的值
     */
    public Long IncrNum(String filePrefix, String key, int delta) throws IllegalArgumentException {
        if (StringUtil.isEmpty(filePrefix) || StringUtil.isEmpty(key)) {
            throw new IllegalArgumentException();
        }
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;

        Long value;
        try {
            value = stringRedisTemplate.opsForValue().increment(realKey, delta);
            if (value == null) {
                return 0L;
            }
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        return value;
    }
    /**
    * @Description: zset中批量添加元素
            * @Param: filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
            * @Param: key 缓存key最后一部分
            * @Param: score 分数，用于排序
            * @Param: json 需要转化的value
            * @return: 结果值
            * @Author: shen.yang
            * @Date:
    */
    public void addZsetValue(String filePrefix, String key, Set values){
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try{
            stringRedisTemplate.opsForZSet().add(realKey,values);
        }
        catch (Exception e) {
            LOG.error("往redis添加" + realKey + "下的数据时出错", e);
            throw e;
        }
    }

    /**
     * @Description: 获取对应zset的size
     * @Param: filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @Param: key 缓存key最后一部分
     * @return: java.lang.Long
     * @Author: shen.yang
     * @Date: 2022/1/11  9:24
    */
    public Long zsetSize(String filePrefix, String key){
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;
        Long size;
        try{
            size = stringRedisTemplate.opsForZSet().zCard(realKey);
        }
        catch (Exception e) {
            LOG.error("从redis获取" + realKey + "下的数据size时出错", e);
            throw e;
        }
        return size;
    }

    /**
     * @Description: 获取对应的zset,按照score从大到小排序(start 0 end -1 : 获取所有元素)
     * @Param: filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @Param: key 缓存key最后一部分
     * @return: java.util.Set<java.lang.Object>
     * @Author: shen.yang
     * @Date: 2022/1/11  9:31
    */
    public Set getZset(String filePrefix, String key ,Long start ,Long end){
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;
        Set values;
        try{
            values = stringRedisTemplate.opsForZSet().reverseRange(realKey, start, end);
        }
        catch (Exception e) {
            LOG.error("从redis获取" + realKey + "下的数据时出错", e);
            throw e;
        }
        return values;
    }

    /**
     * @Description: 删除区间元素
     * @Param: filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @Param: key 缓存key最后一部分
     * @param min score区间的下限
     * @param max score区间的上限
     * @return: java.lang.Long
     * @Author: shen.yang
     * @Date: 2022/1/11  14:07
     */
    public void removeZsetValueByScore(String filePrefix, String key, double min, double max){

        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try {
            stringRedisTemplate.opsForZSet().removeRangeByScore(realKey, min, max);
        } catch (Exception e) {
            LOG.error("从redis删除" + realKey + "下的数据时出错", e);
            throw e;
        }
    }


    /**
     * @Description: 删除区间元素
     * @Param: filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @Param: key 缓存key最后一部分
     * @param start
     * @param end
     * @return: java.lang.Long
     * @Author: shen.yang
     * @Date: 2022/1/11  17:22
     */
    public void removeZsetValueByIndex(String filePrefix, String key, Long start, Long end){

        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;
        try {
            stringRedisTemplate.opsForZSet().removeRange(realKey,start,end);
        } catch (Exception e) {
            LOG.error("从redis删除" + realKey + "下的数据时出错", e);
            throw e;
        }
    }

    /**
     * @Description: 获取指定元素的score
     * @Param: filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @Param: key 缓存key最后一部分
     * @param value 元素值
     * @return: double
     * @Author: shen.yang
     * @Date: 2022/1/11  14:45
    */
    public double getScoreByValue(String filePrefix, String key, String value){
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;
        double score;
        try {
            score = stringRedisTemplate.opsForZSet().score(realKey, value);
        } catch (Exception e) {
            LOG.error("从redis获取" + realKey + "下的数据时出错", e);
            throw e;
        }
        return score;
    }

    /**
     * 查看即将存入的redis KEY是否存在
     * 存在:不存入redis，返回fasle
     * 不存在:存入redis，返回true
     *
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key        缓存key最后一部分
     * @param json       存入的值
     * @param time       过期时间
     * @return 是否存入成功
     */
    public Boolean setIfAbsent(String filePrefix, Object key, Object json, Integer time) {
        String realKey = projectName + ":" + filePrefix + ":" + key;
        boolean absent;
        try {
            String jsonStr = objectValueToString(json);
            if (time != null && time > 0) {
                // 如果有时间传入，则传入时间
                absent = Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(realKey, jsonStr, time, TimeUnit.SECONDS));
            } else {
                absent = Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(realKey, jsonStr, DEFAULT_EFFECTIVE_TIME, TimeUnit.SECONDS));
            }
        } catch (Exception e) {
            LOG.error("redis写入" + realKey + "时出错", e);
            throw e;
        }
        return absent;
    }

    /**
     * 增加小数
     *
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key        缓存key最后一部分
     * @param delta      小数
     * @return 加之后的值
     * @throws IllegalArgumentException 异常
     */
    public BigDecimal incrDecimal(String filePrefix, String key, BigDecimal delta) throws IllegalArgumentException {
        if (StringUtil.isEmpty(filePrefix) || StringUtil.isEmpty(key)) {
            throw new IllegalArgumentException();
        }
        // 拼装实际key
        String realKey = projectName + ":" + filePrefix + ":" + key;

        Double value;
        try {
            value = stringRedisTemplate.opsForValue().increment(realKey, delta.doubleValue());
            if (Objects.isNull(value)) {
                return BigDecimal.ZERO;
            }
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        return BigDecimal.valueOf(value);
    }

    /**
     * 查询是否存在库存并发锁
     *
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key        缓存key最后一部分
     * @return treu -> 存在。false -> 不存在
     * @throws IllegalArgumentException 异常
     */
    public Boolean stockConcurrentLockHashKey(String filePrefix, String key) throws IllegalArgumentException {
        if (StringUtil.isEmpty(filePrefix) || StringUtil.isEmpty(key)) {
            throw new IllegalArgumentException();
        }
        // 拼装实际key 例： {xbbProLocal:productStockLock:}xbbProLocal:{productStockLock}
        String realKey = "{" + projectName + ":" + filePrefix + ":}" + projectName + ":{" + filePrefix + "}:" + key;
        return stringRedisTemplate.hasKey(realKey);
    }

    /**
     * 库存并发增加小数
     *
     * @param filePrefix 缓存key前缀, 请加到{@link RedisPrefixConstant}
     * @param key        缓存key最后一部分
     * @param delta      小数
     * @return 加之后的值
     * @throws IllegalArgumentException 异常
     */
    public BigDecimal stockConcurrentLockIncrDecimal(String filePrefix, String key, BigDecimal delta) throws IllegalArgumentException {
        if (StringUtil.isEmpty(filePrefix) || StringUtil.isEmpty(key)) {
            throw new IllegalArgumentException();
        }
        // 拼装实际key 例： {xbbProLocal:productStockLock:}xbbProLocal:{productStockLock}
        String realKey = "{" + projectName + ":" + filePrefix + ":}" + projectName + ":{" + filePrefix + "}:" + key;

        Double value;
        try {
            value = stringRedisTemplate.opsForValue().increment(realKey, delta.doubleValue());
            if (Objects.isNull(value)) {
                return BigDecimal.ZERO;
            }
        } catch (Exception e) {
            LOG.error("从redis取出" + realKey + "时出错", e);
            throw e;
        }
        return BigDecimal.valueOf(value);
    }

    /**
     * 从redis中批量删除多个key, 传入的是RedisKeyPojo
     */
    public void removeStockValue(String filePrefix,List<String> keys) {
        if (CollectionsUtil.isEmpty(keys)) {
            return;
        }
        List<String> realKeys = new ArrayList<>();
        for (String key : keys) {
            // 拼装实际key 例:
            //{xbbProLocal:productStockLock:}xbbProLocal:{productStockLock}:2176487_459__0_0
            //{xbbProLocal:productStockLock:}xbbProLocal:{productStockLock}:2176487_459_batch_生产日期_保质期

            String realKey = "{" + projectName + ":" + filePrefix + ":}" + projectName + ":{" + filePrefix + "}:" + key;
            realKeys.add(realKey);
        }
        removeRealValue(realKeys);
    }

}